﻿using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Collections;
using System.Collections.Generic;
using BizElements.BusinessLayer;

namespace BizElements.Web
{
    /// <summary>
    /// Creates grid footers which compute and display numeric aggregate values.
    /// </summary>
    [Serializable]
    public abstract class GridFoooterNumericAggregateBase : IBoundDataGridExtender
    {
        #region FieldPermissions.

        // Cloneable.
        string valueFieldName;
        Type valueDataType;
        const string defaultResultFormat = "{0}";
        string resultFormat = defaultResultFormat;
        IMessageFormatter formatter;
        string placementColumn;

        // NonCloneable.
        bool createFooterControlsInPreRenderStage;
        DataGridItem footerItem;

        #endregion

        #region Constructors.

        /// <summary>
        /// Initializes a new instance of <b>GridFoooterNumericAggregateBase</b> class.
        /// </summary>
        /// <param name="valueFieldName">Name of the numeric field/columns to which aggregate is applied.</param>
        /// <param name="valueDataType">Type of values contained in the field. Only numeric types are allowed.</param>
        public GridFoooterNumericAggregateBase(string valueFieldName, Type valueDataType)
            : this(valueFieldName, valueDataType, null, null, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of <b>GridFoooterNumericAggregateBase</b> class.
        /// </summary>
        /// <param name="valueFieldName">Name of the numeric field/columns to which aggregate is applied.</param>
        /// <param name="valueDataType">Type of values contained in the field. Only numeric types are allowed.</param>
        /// <param name="resultFormat">Result string format. Eg: "SUM: {0}". Nullable.</param>
        public GridFoooterNumericAggregateBase(string valueFieldName, Type valueDataType, string resultFormat)
            : this(valueFieldName, valueDataType, resultFormat, null, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of <b>GridFoooterNumericAggregateBase</b> class.
        /// </summary>
        /// <param name="valueFieldName">Name of the numeric field/columns to which aggregate is applied.</param>
        /// <param name="valueDataType">Type of values contained in the field. Only numeric types are allowed.</param>
        /// <param name="resultFormat">Result string format. Eg: "SUM: {0}". Nullable.</param>
        /// <param name="formatter">Formats numbers values. Nullable.</param>
        /// <param name="placementColumn">The name of the field/column in whose footer the computed value will be displayed. Nullable.</param>
        public GridFoooterNumericAggregateBase(string valueFieldName, Type valueDataType, string resultFormat, IMessageFormatter formatter, string placementColumn)
        {
            this.ValueFieldName = valueFieldName;
            this.ValueDataType = valueDataType;
            this.ResultFormat = resultFormat;
            this.Formatter = formatter;
            this.PlacementColumn = placementColumn;
        }

        #endregion

        #region IBoundDataGridExtender.

        /// <summary>
        /// Assigns the grid which the current instance extends.
        /// </summary>
        /// <param name="grid">Grid.</param>
        /// <remarks>Has no effect if invoked after the data has been bound.</remarks>
        public void AssignToGrid(BoundDataGrid grid)
        {
            // ItemCreated + PreRender event combination ensures that extender is applied only once.
            // Otherwise, when DataGrid.DataBind() is invoked more than once, duplicate footer controls would be created.
            grid.ShowFooter = true;
            grid.ItemCreated += new DataGridItemEventHandler(grid_ItemCreated);
            grid.PreRender += new EventHandler(grid_PreRender);
        }

        private void grid_ItemCreated(object sender, DataGridItemEventArgs e)
        {
            if (e.Item.ItemType == ListItemType.Footer)
            {
                this.createFooterControlsInPreRenderStage = true;
                this.footerItem = e.Item;
            }
        }

        private void grid_PreRender(object sender, EventArgs e)
        {
            if (createFooterControlsInPreRenderStage)
            {
                CreateFooterControl(footerItem, (BoundDataGrid)sender);
                this.createFooterControlsInPreRenderStage = false;
            }
        }

        #endregion

        #region Create footer control on ItemCreated event.

        private void CreateFooterControl(DataGridItem footerItem, BoundDataGrid grid)
        {
            bool isBound = (grid.DataSource != null);
            if (!isBound)
                return;

            IEnumerable list = GetList(grid.DataSource);
            object sum = ComputeAggregateValue(list);
            string formattedSum = (this.formatter != null) ? this.formatter.Format(sum) : Convert.ToString(sum);
            string outputText = string.Format(this.resultFormat, formattedSum);
            string displayInColumn = string.IsNullOrEmpty(this.placementColumn) ? this.valueFieldName : this.placementColumn;
            TableCell footerCell = GetCell(grid, footerItem, displayInColumn);
            footerCell.Controls.Add(new LiteralControl(outputText));
        }

        private object ComputeAggregateValue(IEnumerable listOfValuesForAggregation)
        {
            object sum;
            if (TypeUtil.IsInteger(this.ValueDataType))
            {
                List<long> intValues = GetNonNullIntegralValues(listOfValuesForAggregation, this.ValueFieldName);
                sum = ComputeIntegralAggregate(intValues);
            }
            else
            {
                List<decimal> fpValues = GetNonNullFloatingPointValue(listOfValuesForAggregation, this.ValueFieldName);
                sum = ComputeFloatingPointAggregate(fpValues);
            }

            return sum;
        }

        private static IEnumerable GetList(object dataSource)
        {
            bool isEnumerable = (dataSource is IEnumerable);
            bool isListSource = (isEnumerable) ? false : (dataSource is IListSource);
            if (!isEnumerable && !isListSource)
                throw new ArgumentException("Cannot convert or extract a list from the provided data source. Data source must implement IEnumerable or IListSource interface.", "dataSource");

            IEnumerable list;
            if (isEnumerable)
                list = dataSource as IEnumerable;
            else
                list = (dataSource as IListSource).GetList();

            return list;
        }

        private static List<long> GetNonNullIntegralValues(IEnumerable dataSource, string fieldName)
        {
            List<long> intValues = new List<long>();
            foreach (object dataItem in dataSource)
            {
                object fieldValue = DataBinder.Eval(dataItem, fieldName);
                if (fieldValue != null && fieldValue != DBNull.Value)
                    intValues.Add(Convert.ToInt64(fieldValue));
            }

            return intValues;
        }

        private static List<decimal> GetNonNullFloatingPointValue(IEnumerable dataSource, string fieldName)
        {
            List<decimal> fpValues = new List<decimal>();
            foreach (object dataItem in dataSource)
            {
                object fieldValue = DataBinder.Eval(dataItem, fieldName);
                if (fieldValue != null && fieldValue != DBNull.Value)
                    fpValues.Add(Convert.ToDecimal(fieldValue));
            }

            return fpValues;
        }

        private static TableCell GetCell(BoundDataGrid grid, DataGridItem row, string fieldName)
        {
            DataGridColumn col = grid.GetColumn(fieldName);
            int index = grid.Columns.IndexOf(col);
            TableCell cell = row.Cells[index];
            return cell;
        }

        #endregion

        #region Compute aggregate - protected abstract methods.

        /// <summary>
        /// Computes and returns the result of aggregate function as an integral value.
        /// </summary>
        /// <param name="numericValues">A collection of numeric values which are convertible to <see cref="System.Int64"/>.</param>
        /// <returns>Aggregate value.</returns>
        protected abstract long ComputeIntegralAggregate(IEnumerable numericValues);

        /// <summary>
        /// Computes and returns the result of aggregate function as a floating point value.
        /// </summary>
        /// <param name="numericValues">A collection of numeric values which are convertible to <see cref="System.Decimal"/>.</param>
        /// <returns>Aggregate value.</returns>
        protected abstract decimal ComputeFloatingPointAggregate(IEnumerable numericValues);

        #endregion

        #region Properties.

        /// <summary>
        /// Gets or sets the name of the numeric field/columns to which aggregate is applied.
        /// </summary>
        public string ValueFieldName
        {
            get { return this.valueFieldName; }
            set
            {
                if (string.IsNullOrEmpty(value))
                    throw new ArgumentException("Field name is required.");

                this.valueFieldName = value;
            }
        }

        /// <summary>
        /// Gets or sets the type of values contained in the field. Only numeric types are allowed.
        /// </summary>
        public Type ValueDataType
        {
            get { return this.valueDataType; }
            set
            {
                if (!TypeUtil.IsNumber(value))
                    throw new ArgumentException("Only numeric data types are allowed.");

                this.valueDataType = value;
            }
        }

        /// <summary>
        /// Gets or sets the result string format. Eg: "SUM: {0}". Nullable.
        /// </summary>
        /// <value>A string which obeys the same format as the format used by 
        /// <see cref="string.Format(string, object[])"/> method. Default is <b>"{0}"</b>.</value>
        public string ResultFormat
        {
            get { return this.resultFormat; }
            set { this.resultFormat = string.IsNullOrEmpty(value) ? defaultResultFormat : value; }
        }

        /// <summary>
        /// Gets or sets the optional object which formats the numeric values.
        /// </summary>
        public IMessageFormatter Formatter
        {
            get { return this.formatter; }
            set { this.formatter = value; }
        }

        /// <summary>
        /// Gets or sets the optional name of the field/column in whose footer the computed value will be displayed.
        /// </summary>
        /// <value>Field/column name. Default is <b>null</b> indicating that the aggragate will be placed in the same
        /// column as the one from wich the value are originating.</value>
        public string PlacementColumn
        {
            get { return this.placementColumn; }
            set { this.placementColumn = value; }
        }

        #endregion
    }
}